﻿namespace FitnessTracker.Observables
{
    using System;
    using System.Collections.Generic;
    using System.Diagnostics;
    using System.IO.Ports;
    using System.Linq;
    using System.Reactive.Linq;

    public class SerialPortReader : IObservable<byte[]>, IDisposable
    {
        private const int PortBaudRate = 115200;
        private const Parity PortParity = Parity.None;
        private const StopBits PortStopBits = StopBits.One;
        private const byte PortDataBits = 8;
        private const byte ReceivedBytesThreshold = 60;
        private static SerialPortReader uniqueInstanceOfSerialPortReader;
        private readonly SerialPort serialPort = new SerialPort();
        private readonly List<IObserver<byte[]>> serialReaderObservers = new List<IObserver<byte[]>>();
        private readonly IDisposable disposingSerialDataStream;
        
        private SerialPortReader(string comPortName)
        {
            this.serialPort.PortName = comPortName;
            this.serialPort.BaudRate = PortBaudRate;
            this.serialPort.Parity = PortParity;
            this.serialPort.StopBits = PortStopBits;
            this.serialPort.DataBits = PortDataBits;
            this.serialPort.ReceivedBytesThreshold = ReceivedBytesThreshold;

            var serialDataStream = Observable.FromEventPattern<SerialDataReceivedEventArgs>(serialPort, "DataReceived")
                .Where(d => d.EventArgs.EventType == SerialData.Chars);
            disposingSerialDataStream = serialDataStream.Subscribe(_ => this.SendDataToObservers());
        }

        private bool IsConnectedToComPort
        {
            get
            {
                return this.serialPort.IsOpen;
            }
        }

        public static SerialPortReader GetInstance(string comPortName)
        {
            return uniqueInstanceOfSerialPortReader ?? (uniqueInstanceOfSerialPortReader = new SerialPortReader(comPortName));
        }

        public IDisposable Subscribe(IObserver<byte[]> observer)
        {
            if (!IsConnectedToComPort)
            {
                try
                {
                    this.OpenPort();
                }
                catch (Exception ex)
                {
                    this.PropagateExceptionToObservers(ex);
                }
            }

            if (!this.serialReaderObservers.Contains(observer))
            {
                this.serialReaderObservers.Add(observer);
            }

            return new Unsubscriber<byte[]>(this.serialReaderObservers, observer);
        }

        public void Dispose()
        {
            this.disposingSerialDataStream.Dispose();

            if (this.serialPort.IsOpen)
            {
                this.ClosePort();
            }
        }

        private void SendDataToObservers()
        {
            // important to read data from the port even if no observers as subscribed
            // otherwise data piles up on the line
            var buffer = this.ReadFromSerialPort();  
            foreach (var serialReaderObserver in this.serialReaderObservers.ToList())
            {
                serialReaderObserver.OnNext(buffer);
                Debug.WriteLine("SerialPort sent packet to observers");
            }
        }

        private void PropagateExceptionToObservers(Exception ex)
        {
            foreach (var serialReaderObserver in this.serialReaderObservers.ToList())
            {
                serialReaderObserver.OnError(ex);
            }
        }

        private void OpenPort()
        {
            if (this.serialPort.IsOpen)
            {
                this.serialPort.Close();
            }

            this.serialPort.Open();
        }

        private void ClosePort()
        {
            if (this.serialPort.IsOpen)
            {
                this.serialPort.Close();
                foreach (var hxMObserver in this.serialReaderObservers)
                {
                    hxMObserver.OnCompleted();
                }
            }
        }

        private byte[] ReadFromSerialPort()
        {
            var numberOfBytesInBuffer = this.serialPort.BytesToRead;
            var bufferStore = new byte[numberOfBytesInBuffer];
            this.serialPort.Read(bufferStore, 0, numberOfBytesInBuffer);
            return bufferStore;
        } 
    }
}
